/** jfDataLogger
 *
 * @author pquiring
 */

import java.io.*;
import java.util.*;
import java.awt.*;
import java.net.*;
import javax.swing.*;
import javax.swing.table.*;

import javaforce.*;
import javaforce.controls.*;

public class App extends javax.swing.JFrame {

  public static String version = "0.29";

  public static int delays[] = new int[] {
    5, 10, 25, 50, 100, 500, 1000, 3000, 5000, 10000, 30000, 60000, 300000
  };

  public static int ticks[] = new int[] {
    40, 40, 40, 20, 10, 10, 10, 10, 10, 10, 10, 10, 10
  };

  public static String args[];

  /**
   * Creates new form App
   */
  public App() {
    app = this;
    initComponents();
    JFImage icon = new JFImage();
    icon.loadPNG(this.getClass().getClassLoader().getResourceAsStream("jfdatalogger.png"));
    setIconImage(icon.getImage());
    table.setModel(tableModel);
    list.setModel(listModel);
    newProject();
    setTitle("jfDataLogger/" + version);
    JFAWT.centerWindow(this);
    setExtendedState(JFrame.MAXIMIZED_BOTH);
    if (args != null) {
      for(int a=0;a<args.length;a++) {
        load_project(args[a]);
      }
    }
    setScrollBarSize();
  }

  /**
   * This method is called from within the constructor to initialize the form. WARNING: Do NOT modify this code. The content of this method is always regenerated by the Form Editor.
   */
  @SuppressWarnings("unchecked")
  // <editor-fold defaultstate="collapsed" desc="Generated Code">//GEN-BEGIN:initComponents
  private void initComponents() {

    jToolBar1 = new javax.swing.JToolBar();
    jLabel1 = new javax.swing.JLabel();
    newProject = new javax.swing.JButton();
    jSeparator1 = new javax.swing.JToolBar.Separator();
    load = new javax.swing.JButton();
    save = new javax.swing.JButton();
    jSeparator2 = new javax.swing.JToolBar.Separator();
    run = new javax.swing.JButton();
    jSeparator4 = new javax.swing.JToolBar.Separator();
    settings = new javax.swing.JButton();
    jSplitPane1 = new javax.swing.JSplitPane();
    jPanel1 = new javax.swing.JPanel();
    jScrollPane2 = new javax.swing.JScrollPane();
    table = new javax.swing.JTable();
    jToolBar3 = new javax.swing.JToolBar();
    jLabel2 = new javax.swing.JLabel();
    load_data = new javax.swing.JButton();
    save_data = new javax.swing.JButton();
    jSeparator5 = new javax.swing.JToolBar.Separator();
    clear = new javax.swing.JButton();
    jSeparator3 = new javax.swing.JToolBar.Separator();
    jLabel5 = new javax.swing.JLabel();
    csv_log = new javax.swing.JButton();
    csv_save = new javax.swing.JButton();
    jSeparator6 = new javax.swing.JToolBar.Separator();
    jLabel4 = new javax.swing.JLabel();
    saveImage = new javax.swing.JButton();
    img = new javax.swing.JLabel() {
      public void paint(Graphics g) {
        if (logImage == null) return;
        drawImage(g);
      }
    };
    scrollbar = new javax.swing.JScrollBar();
    jPanel2 = new javax.swing.JPanel();
    jToolBar2 = new javax.swing.JToolBar();
    jLabel3 = new javax.swing.JLabel();
    add = new javax.swing.JButton();
    edit = new javax.swing.JButton();
    delete = new javax.swing.JButton();
    jScrollPane1 = new javax.swing.JScrollPane();
    list = new javax.swing.JList<>();

    setDefaultCloseOperation(javax.swing.WindowConstants.DO_NOTHING_ON_CLOSE);
    setTitle("jfDataLogger");
    addComponentListener(new java.awt.event.ComponentAdapter() {
      public void componentResized(java.awt.event.ComponentEvent evt) {
        formComponentResized(evt);
      }
    });
    addWindowListener(new java.awt.event.WindowAdapter() {
      public void windowClosing(java.awt.event.WindowEvent evt) {
        formWindowClosing(evt);
      }
    });

    jToolBar1.setFloatable(false);
    jToolBar1.setRollover(true);

    jLabel1.setText("Project:");
    jToolBar1.add(jLabel1);

    newProject.setText("New");
    newProject.setFocusable(false);
    newProject.setHorizontalTextPosition(javax.swing.SwingConstants.CENTER);
    newProject.setVerticalTextPosition(javax.swing.SwingConstants.BOTTOM);
    newProject.addActionListener(new java.awt.event.ActionListener() {
      public void actionPerformed(java.awt.event.ActionEvent evt) {
        newProjectActionPerformed(evt);
      }
    });
    jToolBar1.add(newProject);
    jToolBar1.add(jSeparator1);

    load.setText("Load");
    load.setFocusable(false);
    load.setHorizontalTextPosition(javax.swing.SwingConstants.CENTER);
    load.setVerticalTextPosition(javax.swing.SwingConstants.BOTTOM);
    load.addActionListener(new java.awt.event.ActionListener() {
      public void actionPerformed(java.awt.event.ActionEvent evt) {
        loadActionPerformed(evt);
      }
    });
    jToolBar1.add(load);

    save.setText("Save");
    save.setFocusable(false);
    save.setHorizontalTextPosition(javax.swing.SwingConstants.CENTER);
    save.setVerticalTextPosition(javax.swing.SwingConstants.BOTTOM);
    save.addActionListener(new java.awt.event.ActionListener() {
      public void actionPerformed(java.awt.event.ActionEvent evt) {
        saveActionPerformed(evt);
      }
    });
    jToolBar1.add(save);
    jToolBar1.add(jSeparator2);

    run.setText("Run");
    run.setFocusable(false);
    run.setHorizontalTextPosition(javax.swing.SwingConstants.CENTER);
    run.setVerticalTextPosition(javax.swing.SwingConstants.BOTTOM);
    run.addActionListener(new java.awt.event.ActionListener() {
      public void actionPerformed(java.awt.event.ActionEvent evt) {
        runActionPerformed(evt);
      }
    });
    jToolBar1.add(run);
    jToolBar1.add(jSeparator4);

    settings.setText("Settings");
    settings.setFocusable(false);
    settings.setHorizontalTextPosition(javax.swing.SwingConstants.CENTER);
    settings.setVerticalTextPosition(javax.swing.SwingConstants.BOTTOM);
    settings.addActionListener(new java.awt.event.ActionListener() {
      public void actionPerformed(java.awt.event.ActionEvent evt) {
        settingsActionPerformed(evt);
      }
    });
    jToolBar1.add(settings);

    jSplitPane1.setDividerLocation(250);

    table.setModel(new javax.swing.table.DefaultTableModel(
      new Object [][] {
        {null, null, null, null},
        {null, null, null, null},
        {null, null, null, null},
        {null, null, null, null}
      },
      new String [] {
        "Title 1", "Title 2", "Title 3", "Title 4"
      }
    ));
    jScrollPane2.setViewportView(table);

    jToolBar3.setRollover(true);

    jLabel2.setText("Data:");
    jToolBar3.add(jLabel2);

    load_data.setText("Load");
    load_data.setFocusable(false);
    load_data.setHorizontalTextPosition(javax.swing.SwingConstants.CENTER);
    load_data.setVerticalTextPosition(javax.swing.SwingConstants.BOTTOM);
    load_data.addActionListener(new java.awt.event.ActionListener() {
      public void actionPerformed(java.awt.event.ActionEvent evt) {
        load_dataActionPerformed(evt);
      }
    });
    jToolBar3.add(load_data);

    save_data.setText("Save");
    save_data.setFocusable(false);
    save_data.setHorizontalTextPosition(javax.swing.SwingConstants.CENTER);
    save_data.setVerticalTextPosition(javax.swing.SwingConstants.BOTTOM);
    save_data.addActionListener(new java.awt.event.ActionListener() {
      public void actionPerformed(java.awt.event.ActionEvent evt) {
        save_dataActionPerformed(evt);
      }
    });
    jToolBar3.add(save_data);
    jToolBar3.add(jSeparator5);

    clear.setText("Clear");
    clear.setFocusable(false);
    clear.setHorizontalTextPosition(javax.swing.SwingConstants.CENTER);
    clear.setVerticalTextPosition(javax.swing.SwingConstants.BOTTOM);
    clear.addActionListener(new java.awt.event.ActionListener() {
      public void actionPerformed(java.awt.event.ActionEvent evt) {
        clearActionPerformed(evt);
      }
    });
    jToolBar3.add(clear);
    jToolBar3.add(jSeparator3);

    jLabel5.setText("CSV:");
    jToolBar3.add(jLabel5);

    csv_log.setText("Log");
    csv_log.setFocusable(false);
    csv_log.setHorizontalTextPosition(javax.swing.SwingConstants.CENTER);
    csv_log.setVerticalTextPosition(javax.swing.SwingConstants.BOTTOM);
    csv_log.addActionListener(new java.awt.event.ActionListener() {
      public void actionPerformed(java.awt.event.ActionEvent evt) {
        csv_logActionPerformed(evt);
      }
    });
    jToolBar3.add(csv_log);

    csv_save.setText("Save");
    csv_save.setFocusable(false);
    csv_save.setHorizontalTextPosition(javax.swing.SwingConstants.CENTER);
    csv_save.setVerticalTextPosition(javax.swing.SwingConstants.BOTTOM);
    csv_save.addActionListener(new java.awt.event.ActionListener() {
      public void actionPerformed(java.awt.event.ActionEvent evt) {
        csv_saveActionPerformed(evt);
      }
    });
    jToolBar3.add(csv_save);
    jToolBar3.add(jSeparator6);

    jLabel4.setText("Image:");
    jToolBar3.add(jLabel4);

    saveImage.setText("Save");
    saveImage.setFocusable(false);
    saveImage.setHorizontalTextPosition(javax.swing.SwingConstants.CENTER);
    saveImage.setVerticalTextPosition(javax.swing.SwingConstants.BOTTOM);
    saveImage.addActionListener(new java.awt.event.ActionListener() {
      public void actionPerformed(java.awt.event.ActionEvent evt) {
        saveImageActionPerformed(evt);
      }
    });
    jToolBar3.add(saveImage);

    img.setMaximumSize(new java.awt.Dimension(32768, 100));
    img.setMinimumSize(new java.awt.Dimension(0, 100));
    img.addMouseListener(new java.awt.event.MouseAdapter() {
      public void mousePressed(java.awt.event.MouseEvent evt) {
        imgMousePressed(evt);
      }
    });

    scrollbar.setOrientation(javax.swing.JScrollBar.HORIZONTAL);
    scrollbar.addAdjustmentListener(new java.awt.event.AdjustmentListener() {
      public void adjustmentValueChanged(java.awt.event.AdjustmentEvent evt) {
        scrollbarAdjustmentValueChanged(evt);
      }
    });

    javax.swing.GroupLayout jPanel1Layout = new javax.swing.GroupLayout(jPanel1);
    jPanel1.setLayout(jPanel1Layout);
    jPanel1Layout.setHorizontalGroup(
      jPanel1Layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
      .addComponent(jScrollPane2, javax.swing.GroupLayout.DEFAULT_SIZE, 824, Short.MAX_VALUE)
      .addComponent(jToolBar3, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE)
      .addComponent(scrollbar, javax.swing.GroupLayout.Alignment.TRAILING, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE)
      .addComponent(img, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE)
    );
    jPanel1Layout.setVerticalGroup(
      jPanel1Layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
      .addGroup(javax.swing.GroupLayout.Alignment.TRAILING, jPanel1Layout.createSequentialGroup()
        .addComponent(jToolBar3, javax.swing.GroupLayout.PREFERRED_SIZE, 25, javax.swing.GroupLayout.PREFERRED_SIZE)
        .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED)
        .addComponent(jScrollPane2, javax.swing.GroupLayout.PREFERRED_SIZE, 178, javax.swing.GroupLayout.PREFERRED_SIZE)
        .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.UNRELATED)
        .addComponent(img, javax.swing.GroupLayout.DEFAULT_SIZE, 554, Short.MAX_VALUE)
        .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED)
        .addComponent(scrollbar, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE))
    );

    jSplitPane1.setRightComponent(jPanel1);

    jToolBar2.setFloatable(false);
    jToolBar2.setRollover(true);

    jLabel3.setText("Tags:");
    jToolBar2.add(jLabel3);

    add.setText("Add");
    add.setFocusable(false);
    add.setHorizontalTextPosition(javax.swing.SwingConstants.CENTER);
    add.setVerticalTextPosition(javax.swing.SwingConstants.BOTTOM);
    add.addActionListener(new java.awt.event.ActionListener() {
      public void actionPerformed(java.awt.event.ActionEvent evt) {
        addActionPerformed(evt);
      }
    });
    jToolBar2.add(add);

    edit.setText("Edit");
    edit.setFocusable(false);
    edit.setHorizontalTextPosition(javax.swing.SwingConstants.CENTER);
    edit.setVerticalTextPosition(javax.swing.SwingConstants.BOTTOM);
    edit.addActionListener(new java.awt.event.ActionListener() {
      public void actionPerformed(java.awt.event.ActionEvent evt) {
        editActionPerformed(evt);
      }
    });
    jToolBar2.add(edit);

    delete.setText("Delete");
    delete.setFocusable(false);
    delete.setHorizontalTextPosition(javax.swing.SwingConstants.CENTER);
    delete.setVerticalTextPosition(javax.swing.SwingConstants.BOTTOM);
    delete.addActionListener(new java.awt.event.ActionListener() {
      public void actionPerformed(java.awt.event.ActionEvent evt) {
        deleteActionPerformed(evt);
      }
    });
    jToolBar2.add(delete);

    jScrollPane1.setViewportView(list);

    javax.swing.GroupLayout jPanel2Layout = new javax.swing.GroupLayout(jPanel2);
    jPanel2.setLayout(jPanel2Layout);
    jPanel2Layout.setHorizontalGroup(
      jPanel2Layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
      .addComponent(jToolBar2, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE)
      .addComponent(jScrollPane1, javax.swing.GroupLayout.DEFAULT_SIZE, 249, Short.MAX_VALUE)
    );
    jPanel2Layout.setVerticalGroup(
      jPanel2Layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
      .addGroup(jPanel2Layout.createSequentialGroup()
        .addComponent(jToolBar2, javax.swing.GroupLayout.PREFERRED_SIZE, 25, javax.swing.GroupLayout.PREFERRED_SIZE)
        .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED)
        .addComponent(jScrollPane1, javax.swing.GroupLayout.DEFAULT_SIZE, 766, Short.MAX_VALUE))
    );

    jSplitPane1.setLeftComponent(jPanel2);

    javax.swing.GroupLayout layout = new javax.swing.GroupLayout(getContentPane());
    getContentPane().setLayout(layout);
    layout.setHorizontalGroup(
      layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
      .addComponent(jToolBar1, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE)
      .addComponent(jSplitPane1, javax.swing.GroupLayout.Alignment.TRAILING)
    );
    layout.setVerticalGroup(
      layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
      .addGroup(layout.createSequentialGroup()
        .addComponent(jToolBar1, javax.swing.GroupLayout.PREFERRED_SIZE, 25, javax.swing.GroupLayout.PREFERRED_SIZE)
        .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED)
        .addComponent(jSplitPane1))
    );

    pack();
  }// </editor-fold>//GEN-END:initComponents

  private void addActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_addActionPerformed
    add();
  }//GEN-LAST:event_addActionPerformed

  private void deleteActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_deleteActionPerformed
    delete();
  }//GEN-LAST:event_deleteActionPerformed

  private void clearActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_clearActionPerformed
    clear();
  }//GEN-LAST:event_clearActionPerformed

  private void loadActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_loadActionPerformed
    load_project();
  }//GEN-LAST:event_loadActionPerformed

  private void saveActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_saveActionPerformed
    save_project();
  }//GEN-LAST:event_saveActionPerformed

  private void newProjectActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_newProjectActionPerformed
    newProject();
  }//GEN-LAST:event_newProjectActionPerformed

  private void editActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_editActionPerformed
    edit();
  }//GEN-LAST:event_editActionPerformed

  private void formWindowClosing(java.awt.event.WindowEvent evt) {//GEN-FIRST:event_formWindowClosing
    if (worker == null) {
      System.exit(0);
    }
  }//GEN-LAST:event_formWindowClosing

  private void csv_logActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_csv_logActionPerformed
    csv_log();
  }//GEN-LAST:event_csv_logActionPerformed

  private void load_dataActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_load_dataActionPerformed
    load_data();
  }//GEN-LAST:event_load_dataActionPerformed

  private void save_dataActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_save_dataActionPerformed
    save_data();
  }//GEN-LAST:event_save_dataActionPerformed

  private void settingsActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_settingsActionPerformed
    show_settings();
  }//GEN-LAST:event_settingsActionPerformed

  private void runActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_runActionPerformed
    run();
  }//GEN-LAST:event_runActionPerformed

  private void scrollbarAdjustmentValueChanged(java.awt.event.AdjustmentEvent evt) {//GEN-FIRST:event_scrollbarAdjustmentValueChanged
    img.repaint();
  }//GEN-LAST:event_scrollbarAdjustmentValueChanged

  private void formComponentResized(java.awt.event.ComponentEvent evt) {//GEN-FIRST:event_formComponentResized
    setScrollBarSize();
  }//GEN-LAST:event_formComponentResized

  private void saveImageActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_saveImageActionPerformed
    save_image();
  }//GEN-LAST:event_saveImageActionPerformed

  private void imgMousePressed(java.awt.event.MouseEvent evt) {//GEN-FIRST:event_imgMousePressed
    update_position(evt.getX());
  }//GEN-LAST:event_imgMousePressed

  private void csv_saveActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_csv_saveActionPerformed
    csv_save();
  }//GEN-LAST:event_csv_saveActionPerformed

  /**
   * @param args the command line arguments
   */
  public static void main(String args[]) {
    App.args = args;
    /* Create and display the form */
    java.awt.EventQueue.invokeLater(new Runnable() {
      public void run() {
        new App().setVisible(true);
      }
    });
  }

  // Variables declaration - do not modify//GEN-BEGIN:variables
  private javax.swing.JButton add;
  private javax.swing.JButton clear;
  private javax.swing.JButton csv_log;
  private javax.swing.JButton csv_save;
  private javax.swing.JButton delete;
  private javax.swing.JButton edit;
  private javax.swing.JLabel img;
  private javax.swing.JLabel jLabel1;
  private javax.swing.JLabel jLabel2;
  private javax.swing.JLabel jLabel3;
  private javax.swing.JLabel jLabel4;
  private javax.swing.JLabel jLabel5;
  private javax.swing.JPanel jPanel1;
  private javax.swing.JPanel jPanel2;
  private javax.swing.JScrollPane jScrollPane1;
  private javax.swing.JScrollPane jScrollPane2;
  private javax.swing.JToolBar.Separator jSeparator1;
  private javax.swing.JToolBar.Separator jSeparator2;
  private javax.swing.JToolBar.Separator jSeparator3;
  private javax.swing.JToolBar.Separator jSeparator4;
  private javax.swing.JToolBar.Separator jSeparator5;
  private javax.swing.JToolBar.Separator jSeparator6;
  private javax.swing.JSplitPane jSplitPane1;
  private javax.swing.JToolBar jToolBar1;
  private javax.swing.JToolBar jToolBar2;
  private javax.swing.JToolBar jToolBar3;
  private javax.swing.JList<String> list;
  private javax.swing.JButton load;
  private javax.swing.JButton load_data;
  private javax.swing.JButton newProject;
  private javax.swing.JButton run;
  private javax.swing.JButton save;
  private javax.swing.JButton saveImage;
  private javax.swing.JButton save_data;
  private javax.swing.JScrollBar scrollbar;
  private javax.swing.JButton settings;
  private javax.swing.JTable table;
  // End of variables declaration//GEN-END:variables

  public static App app;
  public static ArrayList<Tag> tags = new ArrayList<Tag>();
  public static DefaultTableModel tableModel = new DefaultTableModel();
  public static DefaultListModel listModel = new DefaultListModel();
  public static JFImage logImage = new JFImage(1, 510);
  public static Worker worker;
  public static Task task;
  public static int delay;
  public static int tickCounter;
  public static FileOutputStream logger;
  public String projectFile;
  public String dataFile;
  public String logFile;
  public static boolean active;
  public static int duration = -1;  //seconds (1-60) (-1 = continuous)
  public static int speed = 2;  //index into delay array
  public static int samplesLeft;
  public static Tag trigger = new Tag();
  public static int triggerReset = 250;
  public static boolean SOCKSEnable = false;
  public static String SOCKSHost = "";
  private static boolean triggerValid;
  private int span;  //data not in use yet

  public void newProject() {
    tags.clear();
    listModel.clear();
    list.removeAll();
    clear();
    projectFile = null;
    dataFile = null;
    trigger = new Tag();
    triggerReset = 250;
    SOCKSEnable = false;
    SOCKSHost = "";
  }

  public void clear() {
    while (tableModel.getRowCount() > 0) {
      tableModel.removeRow(0);
    }
    logImage.fill(0, 0, logImage.getWidth(), logImage.getHeight(), 0xffffffff);
    img.repaint();
  }

  public static String dl_filters[][] = new String[][] { {"Data Logger Files (*.dl)", "dl"} };
  public static String csv_filters[][] = new String[][] { {"CSV Files (*.csv)", "csv"} };
  public static String data_filters[][] = new String[][] { {"Data Files (*.dat)", "dat"} };
  public static String img_filters[][] = new String[][] { {"Image Files (*.png)", "png"} };

  public void load_project() {
    String filename = JFAWT.getOpenFile(JF.getCurrentPath(), dl_filters);
    if (filename == null) return;
    load_project(filename);
  }

  public void load_project(String filename) {
    newProject();
    projectFile = filename;
    XML xml = new XML();
    xml.read(filename);
    int cnt = xml.root.getChildCount();
    for(int a=0;a<cnt;a++) {
      XML.XMLTag xmltag = xml.root.getChildAt(a);
      String type = xmltag.getName();
      if (type.startsWith("tag")) type = "tag";
      switch (type) {
        case "tag":
          Tag tag = new Tag();
          tag_load(tag, xmltag.content);
          tags.add(tag);
          listModel.addElement(tag.toString());
          break;
        case "config":
          int ccnt = xmltag.getChildCount();
          for(int b=0;b<ccnt;b++) {
            XML.XMLTag cfg = xmltag.getChildAt(b);
            String ctype = cfg.getName();
            String cdata = cfg.content;
            switch (ctype) {
              case "duration":
                duration = Integer.valueOf(cdata);
                if (duration < -1) duration = -1;
                if (duration > 5*60) duration = 5*60;  //5min max
                break;
              case "speed":
                speed = Integer.valueOf(cdata);
                if (speed < 0) speed = 0;
                if (speed > delays.length-1) speed = delays.length-1;
                break;
              case "triggerTag":
                trigger = new Tag();
                tag_load(trigger, cdata);
                break;
              case "triggerReset":
                triggerReset = Integer.valueOf(cdata);
                break;
              case "socksEnable":
                SOCKSEnable = cdata.equals("true");
                break;
              case "socksHost":
                SOCKSHost = cdata;
                break;
            }
          }
          break;
      }
    }
  }

  public void tag_load(Tag tag, String data) {
    String f[] = data.split("[|]");
    tag.host = f[0];
    switch (f[1]) {
      case "S7": tag.type = ControllerType.S7; break;
      case "AB": tag.type = ControllerType.AB; break;
      case "MB": tag.type = ControllerType.MB; break;
      case "NI": tag.type = ControllerType.NI; break;
      case "JF": tag.type = ControllerType.JF; break;
      case "MIC": tag.type = ControllerType.MIC; break;
      default: JFLog.log("Unknown Tag Type:" + f[1]);
    }
    tag.tag = f[2];
    switch (f[3]) {
      case "bit": tag.size = TagType.bit; break;
      case "int8": tag.size = TagType.int8; break;
      case "int16": tag.size = TagType.int16; break;
      case "int32": tag.size = TagType.int32; break;
      case "int64": tag.size = TagType.int64; break;
      case "float32": tag.size = TagType.float32; break;
      case "float64": tag.size = TagType.float64; break;
      default: JFLog.log("Unknown size:" + f[3]);
    }
    if (tag.isFloat()) {
      tag.fmin = Float.valueOf(f[4]);
      tag.fmax = Float.valueOf(f[5]);
    } else {
      tag.min = Integer.valueOf(f[4]);
      tag.max = Integer.valueOf(f[5]);
    }
    tag.color = Integer.valueOf(f[6], 16);
    if (f.length > 7) {
      try {
        tag.desc = URLDecoder.decode(f[7], "UTF-8");
      } catch (Exception e) {
        JFLog.log(e);
        tag.desc = "";
      }
    } else {
      tag.desc = "";
    }
  }

  public String tag_save_short(Tag tag) {
    String ctrl = "";
    switch (tag.type) {
      case ControllerType.S7: ctrl = "S7"; break;
      case ControllerType.AB: ctrl = "AB"; break;
      case ControllerType.MB: ctrl = "MB"; break;
      case ControllerType.NI: ctrl = "NI"; break;
      case ControllerType.JF: ctrl = "JF"; break;
      case ControllerType.MIC: ctrl = "MIC"; break;
    }
    String size = "";
    switch (tag.size) {
      case TagType.bit: size = "bit"; break;
      case TagType.int8: size = "int8"; break;
      case TagType.int16: size = "int16"; break;
      case TagType.int32: size = "int32"; break;
      case TagType.int64: size = "int64"; break;
      case TagType.float32: size = "float32"; break;
      case TagType.float64: size = "float64"; break;
    }
    return tag.host + "|" + ctrl + "|" + tag.tag + "|" + size;
  }

  public String tag_save(Tag tag) {
    String ctrl = "";
    switch (tag.type) {
      case ControllerType.S7: ctrl = "S7"; break;
      case ControllerType.AB: ctrl = "AB"; break;
      case ControllerType.MB: ctrl = "MB"; break;
      case ControllerType.NI: ctrl = "NI"; break;
      case ControllerType.JF: ctrl = "JF"; break;
      case ControllerType.MIC: ctrl = "MIC"; break;
    }
    String size = "";
    switch (tag.size) {
      case TagType.bit: size = "bit"; break;
      case TagType.int8: size = "int8"; break;
      case TagType.int16: size = "int16"; break;
      case TagType.int32: size = "int32"; break;
      case TagType.int64: size = "int64"; break;
      case TagType.float32: size = "float32"; break;
      case TagType.float64: size = "float64"; break;
    }
    if (tag.desc == null) tag.desc = "";
    String desc = "";
    try {
      desc = URLEncoder.encode(tag.desc, "UTF-8");
    } catch (Exception e) {
      JFLog.log(e);
      desc = "";
    }
    return tag.host + "|" + ctrl + "|" + tag.tag + "|" + size + "|" + tag.getmin() + "|" + tag.getmax() + "|" + Integer.toUnsignedString(tag.color, 16) + "|" + desc;
  }

  public void save_project() {
    String filename;
    if (projectFile != null) {
      filename = JFAWT.getSaveFile(projectFile, dl_filters);
    } else {
      filename = JFAWT.getSaveAsFile(JF.getCurrentPath(), dl_filters);
    }
    if (filename == null) return;
    if (!filename.toLowerCase().endsWith(".dl")) {
      filename += ".dl";
    }
    XML xml = new XML();
    xml.setRoot("jfDataLogger", "", "");
    int cnt = tags.size();
    for(int a=0;a<cnt;a++) {
      Tag tag = tags.get(a);
      xml.addTag(xml.root, "tag", "", tag_save(tag));
    }
    XML.XMLTag config = xml.addTag(xml.root, "config", "", "");
    xml.addTag(config, "speed", "", Integer.toString(speed));
    xml.addTag(config, "duration", "", Integer.toString(duration));
    xml.addTag(config, "triggerTag", "", tag_save(trigger));
    xml.addTag(config, "triggerReset", "", Integer.toString(triggerReset));
    xml.addTag(config, "socksEnable", "", Boolean.toString(SOCKSEnable));
    xml.addTag(config, "socksHost", "", SOCKSHost);
    xml.write(filename);
    projectFile = filename;
  }

  public void load_data() {
    if (projectFile == null) {
      JFAWT.showError("Error", "No project loaded");
      return;
    }
    if (tags.size() == 0) {
      JFAWT.showError("Error", "No tags defined");
      return;
    }
    if (duration == -1) {
      JFAWT.showError("Error", "Load/Save data not support with continuous opertion");
      return;
    }
    String filename = JFAWT.getOpenFile(JF.getCurrentPath(), data_filters);
    if (filename == null) return;
    try {
      FileInputStream fis = new FileInputStream(filename);
      byte buf[] = JF.readAll(fis);
      Data data = Data.load(buf);
      fis.close();
      //validate tags
      int tagCount = tags.size();
      if (data.tagCount != tagCount) throw new Exception("wrong data file");
      for(int a=0;a<tagCount;a++) {
        String tag = tag_save_short(tags.get(a));
        if (!tag.equals(data.tags[a])) throw new Exception("invalid tag");
      }
      //load data
      int rowCount = data.rowCount;
      tableModel.setRowCount(0);
      tableModel.setColumnCount(0);
      tableModel.addColumn("timestamp");
      for(int a=0;a<tagCount;a++) {
        tableModel.addColumn(tags.get(a).toString());
      }
      for(int a=0;a<rowCount;a++) {
        tableModel.addRow(data.data[a]);
      }
      updateFullImage();
      img.repaint();
    } catch (Exception e) {
      JFLog.log(e);
      JFAWT.showError("Error", "Invalid data file");
    }
  }

  public static double scaleInt(Tag tag, int value) {
    if (value < tag.min) return 0;
    if (value > tag.max) return 100;
    double delta = tag.max - tag.min;
    double fval = value;
    double fmin = tag.min;
    return ((fval - fmin) / delta * 100.0);
  }
  public static double scaleFloat(Tag tag, float value) {
    if (value < tag.fmin) return 0;
    if (value > tag.fmax) return 100;
    double delta = tag.fmax - tag.fmin;
    double fval = value;
    double fmin = tag.fmin;
    return ((fval - fmin) / delta * 100.0);
  }
  public static double scaleDouble(Tag tag, double value) {
    if (value < tag.fmin) return 0;
    if (value > tag.fmax) return 100;
    double delta = tag.fmax - tag.fmin;
    double fval = value;
    double fmin = tag.fmin;
    return ((fval - fmin) / delta * 100.0);
  }

  public double getValue(Tag tag, String value) {
    if (value == null) value = "0";
    int iv;
    float fv;
    double dv;
    if (tag.isFloat()) {
      if (tag.getSize() == 8) {
        dv = Double.valueOf(value);
        return scaleDouble(tag, dv);
      } else {
        fv = Float.valueOf(value);
        return scaleFloat(tag, fv);
      }
    } else {
      if (tag.size == TagType.bit) {
        return value.equals("0") ? 0 : tag.min;
      } else {
        iv = Integer.valueOf(value);
        return scaleInt(tag, iv);
      }
    }
  }

  public void setScrollBarSize() {
    int viewWidth = img.getWidth();
    int logWidth = logImage.getWidth();
    scrollbar.setMinimum(0);
    if (viewWidth >= logWidth) {
      scrollbar.setMaximum(0);
    } else {
      scrollbar.setMaximum(logWidth - viewWidth);
    }
    scrollbar.setValue(0);
  }

  public void updateFullImage() {
    int rows = tableModel.getRowCount();
    int tickCounter = 0;
    double sv = 0;
    double lsv = 0;
    int tagCount = tags.size();
    logImage.setSize(rows, 510);
    logImage.fill(0, 0, rows, 510, 0xffffffff);
    for(int tagIdx=0;tagIdx<tagCount;tagIdx++) {
      Tag tag = tags.get(tagIdx);
      tickCounter = ticks[speed];
      lsv = 0;
      for(int x=0;x<rows;x++) {
        String value = (String)tableModel.getValueAt(x, tagIdx+1);
        sv = getValue(tag, value);
        int y = 5 + 500 - (int)(sv * 5.0);
        if (y < 5) y = 5;
        if (y >= 505) y = 505 - 1;
        int ly = 5 + 500 - (int)(lsv * 5.0);
        if (ly < 5) ly = 5;
        if (ly >= 505) ly = 505 - 1;
        if (tag.size == TagType.bit) {
          if (!value.equals("0")) {
            logImage.putPixel(x, y, tag.color);
          }
        } else {
          logImage.line(x-1, ly, x, y, tag.color);
        }
        lsv = sv;
        tickCounter--;
        if (tickCounter == 0) {
          tickCounter = ticks[speed];
          logImage.line(x, 0, x, 4, 0x000000);
          logImage.line(x, 505, x, 509, 0x000000);
        }
      }
    }
    setScrollBarSize();
    repaint();
  }

  public void save_data() {
    if (projectFile == null) {
      JFAWT.showError("Error", "No project loaded");
      return;
    }
    if (tags.size() == 0) {
      JFAWT.showError("Error", "No tags defined");
      return;
    }
    if (duration == -1) {
      JFAWT.showError("Error", "Load/Save data not support with continuous opertion");
      return;
    }
    String filename;
    if (dataFile != null) {
      filename = JFAWT.getSaveFile(dataFile, data_filters);
    } else {
      filename = JFAWT.getSaveAsFile(JF.getCurrentPath(), data_filters);
    }
    if (filename == null) return;
    if (!filename.toLowerCase().endsWith(".dat")) {
      filename += ".dat";
    }
    //save header
    Data header = new Data();
    int tagCount = tags.size();
    header.tagCount = tagCount;
    header.tags = new String[tagCount];
    for(int a=0;a<tags.size();a++) {
      header.tags[a] = tag_save_short(tags.get(a));
    }
    int rows = tableModel.getRowCount();
    header.rowCount = rows;
    header.data = new String[rows][];
    int cols = tableModel.getColumnCount();
    for(int a=0;a<rows;a++) {
      String row[] = new String[cols];
      for(int b=0;b<tagCount+1;b++) {
        row[b] = (String)tableModel.getValueAt(a, b);
      }
      header.data[a] = row;
    }
    try {
      byte data[] = header.save();
      FileOutputStream fos = new FileOutputStream(filename);
      fos.write(data);
      fos.close();
    } catch (Exception e) {
      JFLog.log(e);
    }
    dataFile = filename;
  }

  public void save_image() {
    if (projectFile == null) {
      JFAWT.showError("Error", "No project loaded");
      return;
    }
    if (tags.size() == 0) {
      JFAWT.showError("Error", "No tags defined");
      return;
    }
    if (duration == -1) {
      JFAWT.showError("Error", "Load/Save data not support with continuous opertion");
      return;
    }
    String filename = JFAWT.getSaveAsFile(JF.getCurrentPath(), img_filters);
    if (filename == null) return;
    if (!filename.toLowerCase().endsWith(".png")) {
      filename += ".png";
    }
    logImage.savePNG(filename);
  }

  public void show_settings() {
    SettingsDialog dialog = new SettingsDialog(this, true);
    JFAWT.centerWindow(dialog);
    dialog.setVisible(true);
  }

  public void csv_log() {
    if (logFile == null) {
      logFile = JFAWT.getSaveAsFile(JF.getCurrentPath(), csv_filters);
      if (logFile == null) return;
      if (!logFile.toLowerCase().endsWith(".csv")) {
        logFile += ".csv";
      }
      csv_log.setText("Log*");
    } else {
      logFile = null;
      csv_log.setText("Log");
    }
  }

  public String getColNames() {
    StringBuilder sb = new StringBuilder();
    sb.append("timestamp,");
    int cnt = tags.size();
    for(int a=0;a<cnt;a++) {
      if (a > 0) sb.append(",");
      Tag tag = tags.get(a);
      sb.append(tag.toString());
    }
    sb.append("\r\n");
    return sb.toString();
  }

  public void csv_save() {
    String file = JFAWT.getSaveAsFile(JF.getCurrentPath(), csv_filters);
    if (file == null) return;
    if (!file.toLowerCase().endsWith(".csv")) {
      file += ".csv";
    }
    int rows = tableModel.getRowCount();
    int cols = tableModel.getColumnCount();
    StringBuilder sb = new StringBuilder();
    sb.append(getColNames());
    for(int row=0;row<rows;row++) {
      for(int col=0;col<cols;col++) {
        if (col > 0) sb.append(",");
        sb.append((String)tableModel.getValueAt(row, col));
      }
      sb.append("\r\n");
    }
    try {
      FileOutputStream fos = new FileOutputStream(file);
      fos.write(sb.toString().getBytes("UTF-8"));
      fos.close();
    } catch (Exception e) {
      JFLog.log(e);
    }
  }

  public void add() {
    TagDialog dialog = new TagDialog(null, true);
    dialog.setVisible(true);
    if (dialog.accepted()) {
      Tag tag = new Tag();
      dialog.save(tag);
      tags.add(tag);
      listModel.addElement(tag.toString());
    }
    clear();
  }

  public void edit() {
    int idx = list.getSelectedIndex();
    if (idx == -1) return;
    Tag tag = tags.get(idx);
    TagDialog dialog = new TagDialog(null, true);
    dialog.load(tag);
    dialog.setVisible(true);
    if (dialog.accepted()) {
      dialog.save(tag);
    }
    listModel.setElementAt(tags.get(idx).toString(), idx);
    clear();
  }

  public void delete() {
    int idx = list.getSelectedIndex();
    if (idx == -1) return;
    tags.remove(idx);
    listModel.remove(idx);
    clear();
  }

  private boolean hasNITag() {
    int cnt = tags.size();
    for(int a=0;a<cnt;a++) {
      if (tags.get(a).type == ControllerType.NI) return true;
    }
    return false;
  }

  public void run() {
    if (worker == null) {
      if (tags.size() == 0) {
        JFAWT.showError("Error", "No tags defined");
        return;
      }
      if (hasNITag() && !javaforce.controls.ni.DAQmx.loaded) {
        JFAWT.showError("Error", "Project contains NI Tags but NI DLL not found.");
        return;
      }
      setState(true);
      delay = delays[speed];
      tickCounter = ticks[speed];
      if (duration == -1) {
        logImage.setSize(img.getWidth(), 510);
        samplesLeft = -1;
      } else {
        if (delay <= 1000) {
          samplesLeft = duration * (1000 / delay);
        } else {
          samplesLeft = (duration * 1000) / delay;
        }
        logImage.setSize(samplesLeft, 510);
      }
      span = logImage.getWidth();
      setScrollBarSize();
      if (duration != -1) {
        scrollbar.setValue(scrollbar.getMaximum());
      }
      clear();
      if (logFile != null) {
        try {
          logger = new FileOutputStream(logFile);
          logger.write(getColNames().getBytes());
        } catch (Exception e) {
          e.printStackTrace();
          logger = null;
        }
      } else {
        logger = null;
      }
      run.setText("Stop");
      worker = new Worker();
      worker.start();
    } else {
      worker.cancel();
      if (logFile != null) {
        csv_log.setText("Log");
        logFile = null;
      }
    }
  }

  private void setState(boolean running) {
    newProject.setEnabled(!running);
    load.setEnabled(!running);
    save.setEnabled(!running);
    add.setEnabled(!running);
    edit.setEnabled(!running);
    delete.setEnabled(!running);
    csv_log.setEnabled(!running);
    settings.setEnabled(!running);
  }

  private JFImage tmp = new JFImage(1,1);
  private int tmpw = -1, tmph = -1;

  public void drawImage(Graphics g) {
    int imgWidth = img.getWidth();
    int logWidth = logImage.getWidth();
    int w = imgWidth;
    int h = 510;
    int pos = scrollbar.getValue();
    if (w > logWidth) w = logWidth;
    int px[] = logImage.getPixels(pos, 0, w, h);
    if (w != tmpw || h != tmph) {
      tmp.setSize(w, h);
      tmpw = w;
      tmph = h;
    }
    tmp.putPixels(px, 0, 0, w, h, 0);
    g.drawImage(tmp.getImage(), 0, 0, null);
  }

  public void update_position(int mx) {
    int x = scrollbar.getValue() + mx - span;
    if (x < 0) return;
    table.setRowSelectionInterval(x, x);
    table.scrollRectToVisible(new Rectangle(table.getCellRect(x, 0, true)));
  }

  public static void gui(Runnable task) {
    try {
      java.awt.EventQueue.invokeAndWait(task);
    } catch (Exception e) {
      e.printStackTrace();
    }
  }

  public static java.util.Timer timer;
  public static java.util.Timer triggerTimer;
  public static boolean running;

  public static class Worker extends Thread {
    public void run() {
      JFLog.log("connecting to controllers...");
      running = false;
      //create controllers and find fastest timer
      tableModel.setColumnCount(0);
      tableModel.addColumn("timestamp");
      Controller.rate = 1000 / delay;
      active = true;
      System.out.println("rate=" + Controller.rate);
      System.gc();  //ensure all prev connections are closed
      int cnt = tags.size();
      triggerValid = trigger.isValid();
      if (triggerValid) {
        if (SOCKSEnable) {
          trigger.setSOCKS(SOCKSHost);
        } else {
          trigger.setSOCKS(null);
        }
        trigger.start();
        trigger.connect();
      }
      //start tag timers
      for(int a=0;a<cnt;a++) {
        Tag tag = tags.get(a);
        if (SOCKSEnable) {
          tag.setSOCKS(SOCKSHost);
        } else {
          tag.setSOCKS(null);
        }
        tag.delay = delay;
        Tag parent = null;
        for(int b=0;b<a;b++) {
          if (tags.get(b).host.equals(tag.host)) {
            parent = tags.get(b);
            break;
          }
        }
        tag.start(parent);
        tableModel.addColumn(tag.toString());
      }
      //send trigger
      if (triggerValid) {
        JFLog.log("TriggerSet:" + trigger.toString());
        trigger.setValue("1");
      }
      //start timer
      timer = new java.util.Timer();
      task = new Task();
      timer.scheduleAtFixedRate(task, delay, delay);
      if (triggerValid) {
        triggerTimer = new java.util.Timer();
        triggerTimer.schedule(new Task() {public void run() {
          JFLog.log("TriggerReset:" + trigger.toString());
          trigger.setValue("0");
          triggerTimer = null;
        }}, triggerReset);
      }
      JFLog.log("running...");
      running = true;
    }
    public void cancel() {
      if (!running) return;
      active = false;
      timer.cancel();
      timer = null;
      worker = null;
      task = null;
      if (logger != null) {
        try { logger.close(); } catch (Exception e) {}
        logger = null;
      }
      int cnt = tags.size();
      for(int a=0;a<cnt;a++) {
        Tag tag = tags.get(a);
        tag.stop();
      }
      if (triggerValid) {
        trigger.disconnect();
      }
      app.run.setText("Run");
      app.setState(false);
      running = false;
    }
  }
  public static class Task extends TimerTask {
    public String[] row;
    public int idx;
    public int delaycount = 0;
    public String ln;
    public long start = -1;
    public void run() {
      try {
        if (samplesLeft == 0) return;
        int cnt = tags.size();
        row = new String[cnt+1];
        if (start == -1) {
          start = System.nanoTime();
        }
        int timestamp = (int)((System.nanoTime() - start) / 1000000L);
        String now = Long.toString(timestamp);
        row[0] = now;
        ln = now;
        idx = 1;
        for(int a=0;a<cnt;a++) {
          Tag tag = tags.get(a);
          String data = tag.getValue();
          if (data == null) {
            row[idx] = "error";
          } else {
            row[idx] = data;
          }
          ln += ",";
          ln += row[idx];
          idx++;
        }
        ln += "\r\n";
        if (logger != null) {
          logger.write(ln.getBytes());
        }
        gui(() -> {
          if (app.span > 0) app.span--;
          tableModel.addRow(row);
          if (duration == -1 && (tableModel.getRowCount() > app.logImage.getWidth())) {
            tableModel.removeRow(0);
          }
        });
        updateImage();
        delaycount += delay;
        if (delaycount >= 100) {
          gui( () -> {
            //repaint image
            app.img.repaint();
          });
          delaycount = 0;
        }
        if (duration != -1) {
          samplesLeft--;
          if (samplesLeft == 0) {
            gui( () -> {
              App.app.run();
            });
          }
        }
      } catch (Exception e) {
        e.printStackTrace();
      }
    }
    public double sv, lsv;
    public void getValues(Tag tag) {
      String value = tag.getValue();
      if (value == null) value = "0";
      int iv;
      float fv;
      double dv;
      if (tag.isFloat()) {
        if (tag.getSize() == 8) {
          dv = Double.valueOf(value);
          sv = scaleDouble(tag, dv);
        } else {
          fv = Float.valueOf(value);
          sv = scaleFloat(tag, fv);
        }
      } else {
        if (tag.size == TagType.bit) {
          sv = value.equals("0") ? 0 : tag.min;
        } else {
          iv = Integer.valueOf(value);
          sv = scaleInt(tag, iv);
        }
      }
      Double lv = (Double)tag.getData("last");
      if (lv != null) {
        lsv = lv;
      } else {
        lsv = sv;
      }
      tag.setData("last", sv);
    }
    public void updateImage() {
      int x2 = logImage.getWidth() - 1;
      int px[] = logImage.getPixels(1, 0, x2, 510);
      logImage.putPixels(px, 0, 0, x2, 510, 0);
      logImage.line(x2, 0, x2, 509, 0xffffff);
      int cnt = tags.size();
      for(int a=0;a<cnt;a++) {
        Tag tag = tags.get(a);
        getValues(tag);
        int y = 5 + 500 - (int)(sv * 5.0);
        if (y < 5) y = 5;
        if (y >= 505) y = 505 - 1;
        int ly = 5 + 500 - (int)(lsv * 5.0);
        if (ly < 5) ly = 5;
        if (ly >= 505) ly = 505 - 1;
        if (tag.size == TagType.bit) {
          if (!tag.getValue().equals("0")) {
            logImage.putPixel(x2, y, tag.color);
          }
        } else {
          logImage.line(x2-1, ly, x2, y, tag.color);
        }
      }
      tickCounter--;
      if (tickCounter == 0) {
        tickCounter = ticks[speed];
        logImage.line(x2, 0, x2, 4, 0x000000);
        logImage.line(x2, 505, x2, 509, 0x000000);
      }
    }
  }
}
